#ifndef LIBPLATTCP
#define LIBPLATTCP

/***
*	The objects and methods contained here, define and implement a cross platform interface for networking using the TCP protocol
*	As well as dealing with the most common error cases.
*/

#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/fcntl.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>
#include <sstream>
#include <map>

namespace Platinum{
	namespace Net{
		namespace TCP{
			
			/**
			*	TCP::Connection objects contain a socket and address objects
			*	They are used for tracking TCP Sockets and associating them with the remote addresses
			*/

			struct Connection{
				int Socket;				
				struct sockaddr_in Addr;
				Connection()
				{ 
					Socket = -1;
				}
				~Connection()
				{	if(Socket != -1)
					{
						close(Socket);
					}
				}
			};
               typedef std::map<int,Connection*> ConnectionMap;
			/**
			*	The TCP::Initialize function initializes a Connection object 
			*	Initialization includes misc work such as setting address family and setting the socket to non-blocking
			*/
			bool Initialize(Connection* MyConn)
			{
				MyConn->Addr.sin_family = AF_INET;
				
				MyConn->Socket = socket(PF_INET, SOCK_STREAM, 0);
				if(MyConn->Socket == -1)
				{
					throw Error(errno,"Could not create socket");
					return(false);
				}

				// set SO_REUSEADDR on a socket to true (1):
				int optval = 1;
				setsockopt(MyConn->Socket,SOL_SOCKET,SO_REUSEADDR,&optval, sizeof optval);
				return(true);
			}

			/**
			*	TCP::Bind binds the connection to a given port
			*/
			bool Bind(Connection* MyConn, int iPort)
			{
				MyConn->Addr.sin_port = htons(iPort);
                    MyConn->Addr.sin_addr.s_addr = INADDR_ANY;

				if(bind(MyConn->Socket, (struct sockaddr*)&MyConn->Addr, sizeof MyConn->Addr) == -1)
				{
					throw Error(errno,"Could not bind socket");
					return(false);
				}else{
					return(true);
				}
			}

			/**
			*	TCP::SetBlocking sets a Connection to either blocking or non-blocking
			*/
			void SetBlocking(Connection* MyConn, bool state)
			{
				if(!state)
				{
					fcntl(MyConn->Socket, F_SETFL, O_NONBLOCK);
				}else{
					fcntl(MyConn->Socket, F_SETFL, O_SYNC);
				}
			}

			/**
			*	TCP::Connect establishes an out going connection to a given IP on a given port
			*/
			bool Connect(Connection* MyConn,std::string IP,int iPort)
			{
				MyConn->Addr.sin_family = AF_INET;          // host byte order
				MyConn->Addr.sin_port = htons(iPort);   // short, network byte order
				MyConn->Addr.sin_addr.s_addr = inet_addr(IP.c_str());
				memset(MyConn->Addr.sin_zero, '\0', sizeof MyConn->Addr.sin_zero);

				if(connect(MyConn->Socket, (struct sockaddr *)&MyConn->Addr, sizeof MyConn->Addr) == -1)
				{
					std::stringstream msg;
					msg << "Could not connect to remote host "
					<< inet_ntoa(MyConn->Addr.sin_addr)
					<< " on port "
					<< ntohs(MyConn->Addr.sin_port)
					<< " "
					<< errno
					<< std::endl;
					throw Error(errno,msg.str());
					return(false);
				}else{
					return(true);
				}
			}


			/**
			*	TCP::Listen listens to a given Connection waiting for an incoming connection
			*	It will not block and returns a new Connection object if there is a waiting connection 
			*	or it will return the original Connection object if there was no waiting connection
			*/
			Connection* Listen(Connection* MyConn)
			{
                    int Socket = MyConn->Socket;
                    struct sockaddr_in Addr = MyConn->Addr;
                    socklen_t sin_size = sizeof Addr;

				if(listen(MyConn->Socket, BACKLOG) != -1)
                    {				
					Socket = accept(MyConn->Socket, (struct sockaddr *)&Addr, &sin_size);
                         if(Socket != -1)
                         {    
                              Connection* Remote = new Connection;
                              Remote->Socket = Socket;
                              Remote->Addr = Addr;
                              return(Remote);
                         }else{
                              if(errno != EAGAIN && errno != EWOULDBLOCK)
                                   throw(Error(errno," while accepting a new connection"));

                              return(MyConn);
                         }
				}else{
                         if(errno != EAGAIN && errno != EWOULDBLOCK)
                              throw(Error(errno," while listening for new connections"));

                         return(MyConn);
                    }    
			}					

			/**
			*	TCP::Send will send data to a given Connection
			*	It will thow an exception if you pass it an empty string or if the data could not be sent,
			*	or if the connection was closed
			*/
			bool Send(Connection* MyConn, std::string Data)
			{
				int len, sent;
				len = Data.size();
				if(len > 0)
				{
					sent = send(MyConn->Socket, Data.c_str(),len,0);
					if(sent < len && sent != -1)
					{
						std::stringstream msg;
						msg << "Only sent "
						<< sent
						<< " of "
						<< len
						<< " bytes!"
						<< std::endl;

						throw(Error(errno,msg.str()));
						return(false);
					}else{
						return(true);
					}
				}else{
					throw(Error(5,"You are attempting to send an empty string"));
					return(true);
				}
				
			}

			/**
			*	TCP::Recv will recieve data from a connection and return a string containing that data.
			*	It will throw an exception if the connection was closed.
			*/
			std::string Recv(Connection* MyConn)
			{
				std::string msg;
				char buf[MAXBUF+1];
				memset(buf,'\0',MAXBUF);
				int bytes = recv(MyConn->Socket,buf,MAXBUF,0);
				if(bytes == 0)
				{
					throw(Error(errno,"Remote Connection was closed"));
				}else{
					if(bytes != -1)
					{
						msg = buf;
						return(msg);
					}else{
						return(msg);
					}
				}
			}
		}
	}
}
#endif

